package net.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Set;
import java.util.StringTokenizer;

import javax.activation.MimetypesFileTypeMap;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

/**
 * File handler for Sun HttpServer.
 * <p>
 * Uses Java Activation Framework for mime types.
 * </p>
 * 
 * @see javax.activation.MimetypesFileTypeMap
 * @see <a
 *      href="http://java.sun.com/javase/6/docs/api/index.html?javax/activation/MimetypesFileTypeMap.html">javax.activation.MimetypesFileTypeMap</a>
 * @author Marko Niemi
 */
public class FileHttpHandler implements HttpHandler {
	/**
	 * Mime type from Java Activation Framework
	 */
	protected static MimetypesFileTypeMap mimeTypes = new MimetypesFileTypeMap();
	/**
	 * Common mime types for dynamic content
	 */
	public static final String MIME_PLAINTEXT = "text/plain";
	public static final String MIME_HTML = "text/html";
	public static final int SC_INTERNAL_SERVER_ERROR = 500;
	public static final int SC_FORBIDDEN = 403;
	public static final int SC_NOT_FOUND = 404;
	public static final int SC_MOVED_PERMANENTLY = 301;
	public static final int SC_OK = 200;
	/**
	 * HTTPServer root dir
	 */
	protected File homeDir;
	protected boolean allowDirectoryListing;
	// TODO check what logger spring uses
	static final Logger logger = LogManager.getLogger(FileHttpHandler.class
			.getName());

	public FileHttpHandler() {
		this(".", true);
	}

	public FileHttpHandler(final String rootDir, final boolean allowDirectoryListing) {
		this(rootDir, allowDirectoryListing, null);
	}

	public FileHttpHandler(final String rootDir, final boolean allowDirectoryListing,
			final String mimeTypesFile) {
		this.allowDirectoryListing = allowDirectoryListing;
		if (mimeTypesFile != null) {
			try {
				mimeTypes = new MimetypesFileTypeMap(mimeTypesFile);
			} catch (IOException e) {
				logger.error(String.format("Unable to read mime type file %s", mimeTypesFile));
			}
		}
		homeDir = new File(rootDir);
		logger.info(String.format("Root dir is %s", homeDir.getAbsolutePath()));
	}

	/**
	 * Handle HTTP request.
	 * 
	 * @see <a href="http://java.sun.com/javase/6/docs/jre/api/net/httpserver/spec/com/sun/net/httpserver/HttpExchange.html">HttpExchange</a> for details.
	 */
	public void handle(HttpExchange exchange) throws IOException {
		// getRequestMethod() to determine the command
		final String requestMethod = exchange.getRequestMethod();
		if (requestMethod.equalsIgnoreCase("GET")) {
			final URI uri = exchange.getRequestURI();
			// getRequestHeaders() to examine the request headers (if needed)
			final Headers requestHeaders = exchange.getRequestHeaders();
			// getRequestBody() returns a InputStream for reading the request
			// body.
			// After reading the request body, the stream is close.
			exchange.getRequestBody();
			// getResponseHeaders() to set any response headers, except
			// content-length
			Headers responseHeaders = exchange.getResponseHeaders();
			Response response = handleRequest(uri.toString(), requestHeaders,
					responseHeaders, homeDir);
			responseHeaders.set("Content-Type", response.mimeType);
			if (logger.isDebugEnabled()) {
				Set<String> keySet = responseHeaders.keySet();
				logger.debug("Response headers:");
				for (String key : keySet) {
					logger.debug(String.format("%s : %s", key, responseHeaders
							.getFirst(key)));
				}
			}
			// sendResponseHeaders(int,long) to send the response headers. Must
			// be
			// called before next step.
			exchange.sendResponseHeaders(response.statusCode,
					response.contentLength);
			// getResponseBody() to get a OutputStream to send the response
			// body.
			// When the response body has been written, the stream must be
			// closed to
			// terminate the exchange.
			OutputStream responseBody = exchange.getResponseBody();
			copyData(response.data, responseBody);
			responseBody.close();
			exchange.close();
		}
	}

	/**
	 * Serves file from homeDir and its' subdirectories (only). Uses only URI,
	 * ignores all headers and HTTP parameters.
	 */
	public Response handleRequest(String uri, Headers requestHeaders,
			Headers responseHeaders, File homeDir) {
		// Make sure we won't die of an exception later
		if (!homeDir.isDirectory()) {
			return new Response(SC_INTERNAL_SERVER_ERROR,
					MIME_PLAINTEXT,
					"INTERNAL ERRROR: serveFile(): given homeDir is not a directory.");
		}
		// Remove URL arguments
		String realUri = uri;
		realUri.trim().replace(File.separatorChar, '/');
		if (realUri.indexOf('?') >= 0) {
			realUri = realUri.substring(0, realUri.indexOf('?'));
		}
		// Prohibit getting out of current directory
		if (realUri.startsWith("..") || realUri.endsWith("..")
				|| (realUri.indexOf("../") >= 0) ) {
			return new Response(SC_FORBIDDEN,
					MIME_PLAINTEXT,
					"FORBIDDEN: Won't serve ../ for security reasons.");
		}
		File file = new File(homeDir, realUri);
		if (!file.exists()) {
			return new Response(SC_NOT_FOUND,
					MIME_PLAINTEXT, "Error 404, file not found.");
		}
		// List the directory, if necessary
		if (file.isDirectory()) {

			// Browsers get confused without '/' after the
			// directory, send a redirect.
			if (!realUri.endsWith("/")) {
				realUri += "/";
				Response response = new Response(
						SC_MOVED_PERMANENTLY, MIME_HTML,
						"<html><body>Redirected: <a href=\"" + realUri + "\">"
								+ realUri + "</a></body></html>");
				responseHeaders.add("Location", realUri);
				return response;
			}
			// First try index.html and index.htm
			if (new File(file, "index.html").exists()) {
				file = new File(homeDir, realUri + "/index.html");
			} else if (new File(file, "index.htm").exists()) {
				file = new File(homeDir, realUri + "/index.htm");
			}
			// No index file, list the directory
			else if (isAllowDirectoryListing()) {
				String msg = getDirectoryListing(realUri, file);
				return new Response(SC_OK, MIME_HTML, msg);
			} else {
				return new Response(SC_FORBIDDEN,
						MIME_PLAINTEXT, "FORBIDDEN: No directory listing.");
			}
		}

		try {
			// Get MIME type from file name extension, if possible
			String mime = null;
			mime = mimeTypes.getContentType(file);
			// Support (simple) skipping:
			long startFrom = 0;
			String range = requestHeaders.getFirst("Range");
			if (range != null) {
				if (range.startsWith("bytes=")) {
					range = range.substring("bytes=".length());
					int minus = range.indexOf('-');
					if (minus > 0) {
						range = range.substring(0, minus);
					}
					try {
						startFrom = Long.parseLong(range);
					} catch (NumberFormatException nfe) {
						// if number cannot be parsed, skipping is not done
					}
				}
			}

			FileInputStream fis = new FileInputStream(file);
			fis.skip(startFrom);
			Response response = new Response(SC_OK, mime,
					fis);
			response.contentLength = file.length() - startFrom;
			responseHeaders.add("Content-length", "" + response.contentLength);
			responseHeaders.add("Content-range", "bytes " + startFrom + "-"
					+ (file.length() - 1) + "/" + file.length());
			return response;
		} catch (IOException ioe) {
			return new Response(SC_FORBIDDEN,
					MIME_PLAINTEXT, "FORBIDDEN: Reading file failed.");
		}
	}

	private String getDirectoryListing(String uri, File file) {
		String[] files = file.list();
		String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
		if (uri.length() > 1) {
			String u = uri.substring(0, uri.length() - 1);
			int slash = u.lastIndexOf('/');
			if (slash >= 0 && slash < u.length())
				msg += "<b><a href=\"" + uri.substring(0, slash + 1)
						+ "\">..</a></b><br/>";
		}
		for (int i = 0; i < files.length; ++i) {
			File currentFile = new File(file, files[i]);
			boolean directory = currentFile.isDirectory();
			if (directory) {
				msg += "<b>";
				files[i] += "/";
			}
			msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">" + files[i]
					+ "</a>";
			// Show file size
			if (currentFile.isFile()) {
				long len = currentFile.length();
				msg += " &nbsp;<font size=2>(";
				if (len < 1024) {
					msg += currentFile.length() + " bytes";
				} else if (len < 1024 * 1024) {
					msg += currentFile.length() / 1024 + "."
							+ (currentFile.length() % 1024 / 10 % 100) + " KB";
				} else {
					msg += currentFile.length() / (1024 * 1024) + "."
							+ currentFile.length() % (1024 * 1024) / 10 % 100
							+ " MB";
				}
				msg += ")</font>";
			}
			msg += "<br/>";
			if (directory) {
				msg += "</b>";
			}
		}
		return msg;
	}

	/**
	 * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
	 * instead of '+'.
	 */
	private String encodeUri(String uri) {
		String newUri = "";
		StringTokenizer st = new StringTokenizer(uri, "/ ", true);
		while (st.hasMoreTokens()) {
			String tok = st.nextToken();
			if (tok.equals("/")) {
				newUri += "/";
			} else if (tok.equals(" ")) {
				newUri += "%20";
			} else {
				try {
					newUri += URLEncoder.encode(tok, "UTF-8");
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
				}
			}
		}
		logger.debug(String.format("endoced uri: %s", newUri));
		return newUri;
	}

	protected void copyData(InputStream data, OutputStream out)
			throws IOException {
		if (data != null) {
			byte[] buff = new byte[2048];
			boolean eof = false;
			do {
				int read = data.read(buff, 0, 2048);
				if (read <= 0) {
					eof = true;
				}
				out.write(buff, 0, read);
			} while (!eof);
		}
	}

	public void setAllowDirectoryListing(boolean allowDirectoryListing) {
		this.allowDirectoryListing = allowDirectoryListing;
	}

	public boolean isAllowDirectoryListing() {
		return allowDirectoryListing;
	}
}
